import { Code, MapReduceOptions } from "mongodb";

import { IParam } from "./types";
import * as utils from './utils';
import { CursorType } from "./defines";
import { MongoCfg, MongoColls, MongoCollNames } from "../config/mongo";
import { toInt } from './utils';
import { isNotEmpty } from './utils';


// type alias
type FuncOrCode = Function | Code;
type FuncOrString = Function | string;
type ObjOrObjArray = Object | Object[];
type KeysType = ObjOrObjArray|FuncOrCode;

export interface InsertOptions { w?: number | string
}
export interface DeleteOptions { w?: number | string
}
export interface UpdateOptions { w?: number | string, upsert?: boolean
}
export interface FindAndDeleteOptions { project?: Object, sort?: Object
}
export interface FindAndUpdateOptions { project?: Object, sort?: Object, upsert?: boolean
}

export type MapReducingOptions = MapReduceOptions;

export class Param implements IParam {
    isValid() {
        return true;
    }

    toString() {
        return 'Param';
    }
}

export class FilterParam extends Param {
    private _filter: Object = {};

    constructor(filter?: Object) {
        super();
        this._filter = filter;
    }

    get filter(): Object {
        return this._filter;
    }

    set filter(filter: Object) {
        this._filter = filter;
    }

    toString() {
        return 'FilterParam';
    }
}

export class FindOneParam extends FilterParam {
    private _sort: ObjOrObjArray = null;
    private _project: Object     = null;

    constructor(filter?: Object, project?: Object, sort?: Object) {
        super(filter);
        if( isNotEmpty(project) ) {
            this._project = project;
        }
        if( isNotEmpty(sort) ) {
            this._sort = sort;
        }
    }

    get sort(): ObjOrObjArray {
        return this._sort;
    }

    set sort(sort: ObjOrObjArray) {
        this._sort = sort;
    }

    get project(): Object {
        return this._project;
    }

    set project(project: Object) {
        this._project = project;
    }

    toString() {
        return 'FindOneParam';
    }
}

export class FindParam extends FindOneParam {
    private _skip: number;
    private _limit: number;

    constructor(filter?: Object, project?: Object, skip?: number, limit?: number, sort?: Object) {
        super(filter, project, sort);
        skip  = skip && toInt(skip);
        limit = limit && toInt(limit);
        if( skip > 0 ) {
            this._skip = skip;
        }
        if( limit > 0 ) {
            this._limit = limit;
        }
    }

    get skip(): number {
        return this._skip;
    }

    set skip(skip: number) {
        this._skip = utils.toInt(skip);
    }

    get limit(): number {
        return this._limit;
    }

    set limit(limit: number) {
        this._limit = utils.toInt(limit);
    }

    toString() {
        return 'FindParam';
    }
}

export class FindOneDeleteParam extends FindOneParam {
    constructor(filter?: Object) {
        super(filter);
    }

    isValid(): boolean {
        return utils.isNotEmpty(this.filter);
    }

    toString() {
        return 'FindOneDeleteParam';
    }
}

export class FindOneReplaceParam extends FindOneParam {
    private _replacement: Object;
    private _upsert: boolean = false;

    constructor(filter?: Object, replacement?: Object, upsert?: boolean) {
        super(filter);
        this._upsert      = upsert;
        this._replacement = replacement;
    }

    get replacement(): Object {
        return this._replacement;
    }

    set replacement(replacement: Object) {
        this._replacement = replacement;
    }

    get upsert(): boolean {
        return this._upsert;
    }

    set upsert(upsert: boolean) {
        this._upsert = upsert;
    }

    toString() {
        return 'FindOneReplaceParam';
    }

    isValid(): boolean {
        return utils.isNotEmpty(this.filter) && utils.isObject(this._replacement);
    }
}

export class FindOneUpdateParam extends FindOneParam {
    private _update: Object;
    private _upsert: boolean = false;

    constructor(filter?: Object, update?: Object, upsert?: boolean) {
        super(filter, {});
        this._update = update;
        this._upsert = upsert;
    }

    get update(): Object {
        return this._update;
    }

    set update(update: Object) {
        this._update = update;
    }

    get upsert(): boolean {
        return this._upsert;
    }

    set upsert(upsert: boolean) {
        this._upsert = upsert;
    }

    toString(): string {
        return 'FindOneUpdateParam';
    }

    isValid(): boolean {
        return utils.isNotEmpty(this.filter) && utils.isObject(this._update);
    }
}

export class InsertParam<T> extends Param {
    private _doc: T;
    private _options: Object = null;

    constructor(doc: T, options?: InsertOptions) {
        super();
        this._doc     = doc;
        this._options = options;
    }

    get doc(): T {
        return this._doc;
    }

    set doc(doc: T) {
        this._doc = doc;
    }

    get options(): InsertOptions {
        return this._options;
    }

    set options(options: InsertOptions) {
        this._options = options;
    }

    toString(): string {
        return 'InsertParam';
    }

    isValid(): boolean {
        return utils.isObject(this._doc) || utils.isArray(this._doc);
    }
}

export class UpdateParam extends FilterParam {
    private _update: Object;
    private _options: Object = null;

    constructor(filter?: Object, updates?: Object, options?: UpdateOptions) {
        super(filter);
        this._update  = updates;
        this._options = options;
    }

    get update(): Object {
        return this._update;
    }

    set update(update: Object) {
        this._update = update;
    }

    get options(): UpdateOptions {
        return this._options;
    }

    set options(options: UpdateOptions) {
        this._options = options;
    }

    toString(): string {
        return 'UpdateParam';
    }

    isValid(): boolean {
        return this.filter && utils.isDefined(this._update);
    }
}

export class DeleteParam extends FilterParam {
    private _options: Object;

    constructor(filter?: Object, options?: DeleteOptions) {
        super(filter);
        this._options = options;
    }

    get options(): DeleteOptions {
        return this._options;
    }

    set options(options: DeleteOptions) {
        this._options = options;
    }

    toString(): string {
        return 'DeleteParam';
    }

    isValid(): boolean {
        return utils.isNotEmpty(this.filter);
    }
}

export class CountParam extends FilterParam {
    constructor(filter?: Object) {
        super(filter);
    }

    toString(): string {
        return 'CountParam';
    }

    isValid(): boolean {
        return utils.isDefined(this.filter);
    }
}

export class GroupParam extends Param {
    private _keys: KeysType;
    private _initial: Object;
    private _condition: Object;
    private _reduce: FuncOrCode;
    private _finalize: FuncOrCode;
    private _command: boolean;

    constructor(keys?: KeysType) {
        super();
        this._keys = keys;
    }

    get keys(): KeysType {
        return this._keys;
    }

    set keys(keys: KeysType) {
        this._keys = keys;
    }

    get condition(): Object {
        return this._condition;
    }

    set condition(condition: Object) {
        this._condition = condition;
    }

    get initial(): Object {
        return this._initial;
    }

    set initial(initial: Object) {
        this._initial = initial;
    }

    get reduce(): FuncOrCode {
        return this._reduce;
    }

    set reduce(reduce: FuncOrCode) {
        this._reduce = reduce;
    }

    get finalize(): FuncOrCode {
        return this._finalize;
    }

    set finalize(finalize: FuncOrCode) {
        this._finalize = finalize;
    }

    get command(): boolean {
        return this._command;
    }

    set command(command: boolean) {
        this._command = command;
    }

    toString(): string {
        return 'GroupParam';
    }

    isValid(): boolean {
        return utils.isDefined(this._keys);
    };
}

export class DistinctParam extends FilterParam {
    private _key: string;

    constructor(key?: string, filter?: Object) {
        super(filter);
        this._key = key;
    }

    get key(): string {
        return this._key;
    }

    set key(key: string) {
        this._key = key;
    }

    toString(): string {
        return 'DistinctParam';
    }

    isValid(): boolean {
        return !!this._key
    }
}

export class MapReduceParam extends Param {
    private _map: FuncOrString;
    private _reduce: FuncOrString;
    private _options: MapReducingOptions = null;

    constructor(map?: FuncOrString, reduce?: FuncOrString, options?: MapReducingOptions) {
        super();
        this._map     = map;
        this._reduce  = reduce;
        this._options = options;
    }

    get map(): FuncOrString {
        return this._map;
    }

    set map(map: FuncOrString) {
        this._map = map;
    }

    get reduce(): FuncOrString {
        return this._reduce;
    }

    set reduce(reduce: FuncOrString) {
        this._reduce = reduce;
    }

    get options(): MapReducingOptions {
        return this._options;
    }

    set options(options: MapReducingOptions) {
        this._options = options;
    }

    toString(): string {
        return 'DistinctParam';
    }

    isValid(): boolean {
        return utils.isDefined(this._map) && utils.isDefined(this._reduce);
    }
}

export class AggregateParam extends Param {
    private _pipeline: Object[] = [];

    push(type: CursorType, item: any): AggregateParam {
        let stage: Object = null;
        switch( type ) {
            case CursorType.PROJECT:
                stage = utils.isNotEmpty(item) ? { $project: item } : null;
                break;
            case CursorType.MATCH:
                stage = utils.isObject(item) ? { $match: item } : null;
                break;
            case CursorType.REDACT:
                stage = utils.isNotEmpty(item) ? { $redact: item } : null;
                break;
            case CursorType.LIMIT:
                let limit = utils.isNumber(item) && utils.toInt(item) || 0;
                stage     = limit > 0 ? { $limit: limit } : null;
                break;
            case CursorType.SKIP:
                let skip = utils.isNumber(item) && utils.toInt(item) || 0;
                stage    = skip >= 0 ? { $skip: skip } : null;
                break;
            case CursorType.UNWIND:
                stage = (utils.isString(item) && item !== '') ? { $unwind: item } : null;
                break;
            case CursorType.GROUP:
                stage = utils.isNotEmpty(item) ? { $group: item } : null;
                break;
            case CursorType.SORT:
                stage = utils.isNotEmpty(item) ? { $sort: item } : null;
                break;
            case CursorType.GEONEAR:
                stage = utils.isNotEmpty(item) ? { $geoNear: item } : null;
                break;
            case CursorType.LOOKUP:
                item.from = MongoCollNames[item.from];
                stage     = utils.isNotEmpty(item) ? { $lookup: item } : null;
                break;
            case CursorType.OUT:
                stage = (utils.isString(item) && item !== '') ? { $out: item } : null;
                break;
            default:
                throw new Error('CursorType Error');
        }
        if( !stage ) {
            throw new Error('value parameter is not correct!');
        }

        this._pipeline.push(stage);
        return this;
    }

    pop(): Object {
        return this._pipeline.pop();
    }

    // It's better not to call this directly, instead, using push() with parameter validation.
    set pipeline(stages: Object[]) {
        this._pipeline = stages;
    }

    get pipeline(): Object[] {
        return this._pipeline;
    }

    toString(): string {
        return JSON.stringify(this._pipeline);
    }

    isValid(): boolean {
        return this._pipeline.length > 0;
    }
}
